17. 格式内容与逻辑错误处理

数据错误:比缺失值更危险的隐患

在金融数据分析中,除了缺失值外,错误数据同样普遍且危害更大。

错误数据主要分为两类:

  • 格式错误:数据存储形式不正确(如日期、数值格式混乱)
  • 逻辑错误:数据值违反业务规则(如负价格、异常涨跌幅)

逻辑错误直接破坏了数据的准确性有效性,可能导致严重的投资决策失误。

格式错误的常见类型

错误类型 示例 影响
日期格式混乱 ‘2024-01-01’ 与 ‘2024/01/01’ 混用 时间排序失败
数值类型错误 价格存储为字符串 ‘100.5’ 无法进行数学运算
千分位符号 ‘12,000’ 含逗号 转换数值报错
单位不一致 ‘元’ 与 ‘万元’ 混用 计算结果偏差巨大

逻辑错误的常见类型

错误类型 示例 检测方法
负价格 股票价格为 -50 价格 ≤ 0
异常高价 PE 超过 1000 阈值检测
成交量异常 成交量 > 流通股本 交叉验证
违反约束 最高价 < 最低价 逻辑规则检验

数据质量的六个维度

数据质量管理理论将数据质量分为六个维度(Wang & Strong, 1996):

  • 准确性(Accuracy):数据正确反映现实
  • 完整性(Completeness):无缺失值
  • 一致性(Consistency):数据间无矛盾
  • 时效性(Timeliness):数据及时更新
  • 有效性(Validity):符合业务规则
  • 唯一性(Uniqueness):无重复记录

⭐ 平台任务解答代码

Listing 1
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
import numpy as np  # 导入NumPy数值计算库
# 从Excel文件读取数据存入df
df=pd.read_excel('https://huoran.oss-cn-shenzhen.aliyuncs.com/20230313/xls/1635115220651761664.xls',sheet_name='Sheet1')
df1=df.drop([247])  # 删除指定行或列
df1.dropna(how='all',inplace=True)  # 删除全部为空值的行
# 修改日期并转换数据类型
df1.iloc[201,0]='2022-11-04'
df1['日期']=pd.to_datetime(df1['日期'])  # 转换为日期时间格式
# 修改指定值
df1.loc[138,'科创50']=np.nan
# 填充缺失值
df1.fillna(method='ffill',inplace=True)
print(df1)  # 输出数据框数据
            日期       上证综指       科创50
0   2022-01-04  3632.3289  1364.9894
1   2022-01-05  3595.1761  1333.3348
2   2022-01-06  3586.0792  1320.9350
3   2022-01-07  3579.5427  1305.4918
4   2022-01-10  3593.5187  1318.2665
..         ...        ...        ...
237 2022-12-26  3065.5626   951.1103
238 2022-12-27  3095.5678   963.1355
239 2022-12-28  3087.3997   957.2743
240 2022-12-29  3073.7016   962.2530
241 2022-12-30  3089.2579   959.9046

[242 rows x 3 columns]

日期格式统一:问题与挑战

金融数据常来源于多个系统,日期格式不统一:

格式名称 示例 说明
ISO标准 ‘2024-01-01’ 最推荐的格式
斜杠分隔 ‘2024/01/02’ 常见于Excel
欧洲格式 ‘01-03-2024’ DD-MM-YYYY
点号分隔 ‘2024.01.04’ 不常见
紧凑格式 ‘20240105’ 无分隔符

核心工具:pd.to_datetime() 函数

日期格式统一:代码实现

Listing 2
import pandas as pd
import numpy as np

# 创建含多种日期格式的数据
data = {
    'Date': ['2024-01-01',   # ISO标准
             '2024/01/02',   # 斜杠分隔
             '01-03-2024',   # 欧洲格式
             '2024.01.04',   # 点号分隔
             '20240105'],    # 紧凑格式
    'Price': [100.5, 101.2, 99.8, 102.3, 101.5],
    'Volume': [10000, 15000, 12000, 18000, 14000]
}
df_dates = pd.DataFrame(data)

print('原始数据(日期格式混乱):')
print(df_dates)
print('\n数据类型:')
print(df_dates.dtypes)
原始数据(日期格式混乱):
         Date  Price  Volume
0  2024-01-01  100.5   10000
1  2024/01/02  101.2   15000
2  01-03-2024   99.8   12000
3  2024.01.04  102.3   18000
4    20240105  101.5   14000

数据类型:
Date       object
Price     float64
Volume      int64
dtype: object

方法1:pd.to_datetime 自动解析

Listing 3
# errors='coerce': 无法解析的格式转为 NaT(Not a Time)
df_dates['Date_Parsed'] = pd.to_datetime(df_dates['Date'], errors='coerce')
print('自动解析后:')
print(df_dates[['Date', 'Date_Parsed']])
print('\n解析失败(转换为NaT):')
print(df_dates[df_dates['Date_Parsed'].isna()][['Date']])
自动解析后:
         Date Date_Parsed
0  2024-01-01  2024-01-01
1  2024/01/02         NaT
2  01-03-2024         NaT
3  2024.01.04         NaT
4    20240105         NaT

解析失败(转换为NaT):
         Date
1  2024/01/02
2  01-03-2024
3  2024.01.04
4    20240105
  • '01-03-2024' 无法确定是1月3日还是3月1日,转为 NaT
  • 其余格式均能自动识别

方法2 & 3:混合格式与特殊格式

Listing 4
# 方法2: format='mixed' 允许混合多种格式
df_dates['Date_Format2'] = pd.to_datetime(
    df_dates['Date'], format='mixed', errors='coerce'
)
print('使用format="mixed"参数:')
print(df_dates[['Date', 'Date_Format2']])

# 方法3: 对特殊格式明确指定
special_dates = pd.to_datetime('20240105', format='%Y%m%d')
print(f'\n特殊格式解析: {special_dates}')
使用format="mixed"参数:
         Date Date_Format2
0  2024-01-01   2024-01-01
1  2024/01/02   2024-01-02
2  01-03-2024   2024-01-03
3  2024.01.04   2024-01-04
4    20240105   2024-01-05

特殊格式解析: 2024-01-05 00:00:00

errors 参数的三种模式

参数值 行为 适用场景
'raise'(默认) 遇到无法解析的值抛出异常 数据质量有保证时
'coerce' 无法解析转为 NaT 数据质量不确定时(推荐)
'ignore' 保留原值,不转换 需要保留原始数据时

NaT 处理方法

  • 检测:pd.isna()pd.isnull()
  • 处理:删除或手动修正

数值类型转换:问题场景

从 Excel 或 CSV 导入的数据,数值列常被识别为文本:

  • 价格列含错误值 'error' → 直接 astype(float) 会报错
  • PE 列含 'N/A' → 无法转换为数值
  • 成交量含千分位逗号 '12,000' → 转换失败

核心解决方案pd.to_numeric() + errors='coerce'

数值类型转换:代码实现

Listing 5
import pandas as pd

data = {
    'Code': ['600001', '600002', '600003', '600004'],
    'Price': ['100.5', '105.2', 'error', '98.7'],
    'PE': ['15.3', 'N/A', '18.5', '22.1'],
    'Volume': ['10000', '15000', '12,000', '18,000']
}
df_numeric = pd.DataFrame(data)

print('原始数据(数值为文本):')
print(df_numeric)
print('\n数据类型:')
print(df_numeric.dtypes)
原始数据(数值为文本):
     Code  Price    PE  Volume
0  600001  100.5  15.3   10000
1  600002  105.2   N/A   15000
2  600003  error  18.5  12,000
3  600004   98.7  22.1  18,000

数据类型:
Code      object
Price     object
PE        object
Volume    object
dtype: object

pd.to_numeric vs astype

Listing 6
# 错误方式: astype(float) 遇到 'error' 会崩溃
# df_numeric['Price'] = df_numeric['Price'].astype(float)  # ValueError!

# 正确方式: pd.to_numeric + errors='coerce'
df_numeric['Price_Num'] = pd.to_numeric(df_numeric['Price'], errors='coerce')
df_numeric['PE_Num'] = pd.to_numeric(df_numeric['PE'], errors='coerce')

print('转换后(无法转换的变为NaN):')
print(df_numeric[['Code', 'Price', 'Price_Num', 'PE', 'PE_Num']])
print('\n转换后数据类型:')
print(df_numeric[['Price_Num', 'PE_Num']].dtypes)
转换后(无法转换的变为NaN):
     Code  Price  Price_Num    PE  PE_Num
0  600001  100.5      100.5  15.3    15.3
1  600002  105.2      105.2   N/A     NaN
2  600003  error        NaN  18.5    18.5
3  600004   98.7       98.7  22.1    22.1

转换后数据类型:
Price_Num    float64
PE_Num       float64
dtype: object

处理千分位逗号

Listing 7
# 先移除逗号,再转换为数值
df_numeric['Volume_Cleaned'] = df_numeric['Volume'].str.replace(',', '')
df_numeric['Volume_Num'] = pd.to_numeric(
    df_numeric['Volume_Cleaned'], errors='coerce'
)

print('处理千分位后:')
print(df_numeric[['Volume', 'Volume_Num']])
处理千分位后:
   Volume  Volume_Num
0   10000       10000
1   15000       15000
2  12,000       12000
3  18,000       18000

A股数据常见格式问题

  • 价格含货币符号(¥100.5)
  • 成交量含 ‘万’、‘亿’ 单位
  • 百分比存储为 ‘12.5%’ 文本

综合格式清洗:创建脏数据

Listing 8
import pandas as pd
import numpy as np

data_dirty = {
    'Date': ['2024-01-01', '2024/01/02', '20240103',
             '2024-01-04', 'invalid'],
    'Code': ['600001.SH', '600002.SH', '000001.SZ',
             '600004.SH', '600005.SH'],
    'Open': ['¥10.5', '11.2', 'error', '12.5', '10.8'],
    'Close': ['10.8', '¥11.5', '12.3', '12.7', '10.5%'],
    'Volume': ['10000', '15,000', '12,000股', '18000', '20万'],
    'PE': ['15.3', 'N/A', '18.5', '>100', '22.1']
}
df_dirty = pd.DataFrame(data_dirty)

print('=== 原始数据(含多种格式错误) ===')
print(df_dirty)
=== 原始数据(含多种格式错误) ===
         Date       Code   Open  Close   Volume    PE
0  2024-01-01  600001.SH  ¥10.5   10.8    10000  15.3
1  2024/01/02  600002.SH   11.2  ¥11.5   15,000   N/A
2    20240103  000001.SZ  error   12.3  12,000股  18.5
3  2024-01-04  600004.SH   12.5   12.7    18000  >100
4     invalid  600005.SH   10.8  10.5%      20万  22.1

综合清洗流程:步骤1-2

Listing 9
df_clean = df_dirty.copy()

# 步骤1:日期格式统一
df_clean['Date'] = pd.to_datetime(df_clean['Date'], errors='coerce')
print('步骤1: 日期格式统一')
print(f'解析失败: {df_clean["Date"].isna().sum()}\n')

# 步骤2:移除货币符号并转换为数值
for col in ['Open', 'Close']:
    df_clean[col] = (df_clean[col].astype(str)
                     .str.replace('¥', '')
                     .str.replace('$', '')
                     .str.replace('%', ''))
    df_clean[col] = pd.to_numeric(df_clean[col], errors='coerce')

print('步骤2: 价格列清洗')
print(df_clean[['Open', 'Close']].head())
步骤1: 日期格式统一
解析失败: 3 条

步骤2: 价格列清洗
   Open  Close
0  10.5   10.8
1  11.2   11.5
2   NaN   12.3
3  12.5   12.7
4  10.8   10.5

综合清洗流程:步骤3-4

Listing 10
# 步骤3:成交量清洗(自定义函数处理多种单位)
def clean_volume(vol_str):
    '''清洗成交量数据,处理万/亿/股等单位'''
    if pd.isna(vol_str):
        return np.nan
    vol_str = str(vol_str).strip()
    if '万' in vol_str:
        return float(vol_str.replace('万', '').replace(',', '')) * 10000
    elif '亿' in vol_str:
        return float(vol_str.replace('亿', '').replace(',', '')) * 100000000
    elif '股' in vol_str:
        return float(vol_str.replace('股', '').replace(',', ''))
    else:
        return float(vol_str.replace(',', ''))

df_clean['Volume_Num'] = df_clean['Volume'].apply(clean_volume)
print('步骤3: 成交量清洗')
print(df_clean[['Volume', 'Volume_Num']])

# 步骤4:PE比率清洗
df_clean['PE'] = df_clean['PE'].astype(str).str.replace('>', '')
df_clean['PE'] = pd.to_numeric(df_clean['PE'], errors='coerce')
print('\n步骤4: PE比率清洗')
print(df_clean[['PE']].head())
步骤3: 成交量清洗
    Volume  Volume_Num
0    10000     10000.0
1   15,000     15000.0
2  12,000股     12000.0
3    18000     18000.0
4      20万    200000.0

步骤4: PE比率清洗
      PE
0   15.3
1    NaN
2   18.5
3  100.0
4   22.1

清洗后验证

Listing 11
print('=== 清洗后数据类型 ===')
print(df_clean.dtypes)

print('\n=== 清洗后数据 ===')
print(df_clean.head())

print('\n=== 数据质量报告 ===')
print(f'总行数: {len(df_clean)}')
print(f'完整行数: {df_clean.dropna().shape[0]}')
print('\n各列缺失值统计:')
print(df_clean.isnull().sum())
=== 清洗后数据类型 ===
Date          datetime64[ns]
Code                  object
Open                 float64
Close                float64
Volume                object
PE                   float64
Volume_Num           float64
dtype: object

=== 清洗后数据 ===
        Date       Code  Open  Close   Volume     PE  Volume_Num
0 2024-01-01  600001.SH  10.5   10.8    10000   15.3     10000.0
1        NaT  600002.SH  11.2   11.5   15,000    NaN     15000.0
2        NaT  000001.SZ   NaN   12.3  12,000股   18.5     12000.0
3 2024-01-04  600004.SH  12.5   12.7    18000  100.0     18000.0
4        NaT  600005.SH  10.8   10.5      20万   22.1    200000.0

=== 数据质量报告 ===
总行数: 5
完整行数: 2

各列缺失值统计:
Date          3
Code          0
Open          1
Close         0
Volume        0
PE            1
Volume_Num    0
dtype: int64

逻辑错误检测:价格异常

基于业务规则检测价格数据中不可能的值:

  • 规则1:价格不能为负或零(\(Price_t > 0\)
  • 规则2:价格不能超过前一日的 300%(异常涨幅)
  • 规则3:价格不能低于前一日的 30%(异常跌幅)

核心思路:将业务规则转化为布尔条件,筛选违规记录

价格异常检测:代码实现

Listing 12
import pandas as pd
import numpy as np

np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=20)
normal_prices = 100 + np.random.randn(20).cumsum() * 0.5

# 人为添加异常值
prices = normal_prices.copy()
prices[5] = -50      # 异常1:负价格
prices[10] = 5000    # 异常2:异常高价
prices[15] = 0.01    # 异常3:接近零

df_prices = pd.DataFrame({
    'Date': dates, 'Price': prices,
    'Volume': np.random.randint(10000, 100000, 20)
})

# 异常检测规则
rule1 = df_prices['Price'] <= 0
df_prices['Prev_Price'] = df_prices['Price'].shift(1)
rule2 = df_prices['Price'] > df_prices['Prev_Price'] * 3
rule3 = df_prices['Price'] < df_prices['Prev_Price'] * 0.3
anomaly_mask = rule1 | rule2 | rule3

print(f'异常记录数: {anomaly_mask.sum()}')
anomaly_records = df_prices[anomaly_mask].copy()
anomaly_records['Reason'] = ''
anomaly_records.loc[rule1, 'Reason'] = '非正价格'
anomaly_records.loc[rule2, 'Reason'] = '异常涨幅'
anomaly_records.loc[rule3, 'Reason'] = '异常跌幅'
print('\n异常记录详情:')
print(anomaly_records[['Date', 'Price', 'Reason']])
异常记录数: 6

异常记录详情:
         Date        Price Reason
5  2024-01-06   -50.000000   异常跌幅
6  2024-01-07   101.820045   异常涨幅
10 2024-01-11  5000.000000   异常涨幅
11 2024-01-12   101.775732   异常跌幅
15 2024-01-16     0.010000   异常跌幅
16 2024-01-17    99.290055   异常涨幅

异常处理的三种策略

Listing 13
# 策略1:删除异常记录
df_clean_drop = df_prices[~anomaly_mask].copy()
print('策略1: 删除异常记录')
print(f'原始记录数: {len(df_prices)}, 清洗后: {len(df_clean_drop)}\n')

# 策略2:用前值填充
df_clean_ffill = df_prices.copy()
df_clean_ffill.loc[anomaly_mask, 'Price'] = np.nan
df_clean_ffill['Price'] = df_clean_ffill['Price'].ffill()
print('策略2: 前值填充')
print(df_clean_ffill.loc[anomaly_mask, ['Date', 'Price']])

# 策略3:标记但不删除(保留用于人工审核)
df_marked = df_prices.copy()
df_marked['Is_Anomaly'] = anomaly_mask
print('\n策略3: 标记异常')
print(df_marked[df_marked['Is_Anomaly']][['Date', 'Price']])
策略1: 删除异常记录
原始记录数: 20, 清洗后: 14

策略2: 前值填充
         Date       Price
5  2024-01-06  101.147507
6  2024-01-07  101.147507
10 2024-01-11  102.240306
11 2024-01-12  102.240306
15 2024-01-16  100.077614
16 2024-01-17  100.077614

策略3: 标记异常
         Date        Price
5  2024-01-06   -50.000000
6  2024-01-07   101.820045
10 2024-01-11  5000.000000
11 2024-01-12   101.775732
15 2024-01-16     0.010000
16 2024-01-17    99.290055

异常检测的统计学方法

除了业务规则检测,还可以使用统计方法:

Z-Score 方法

\[Z = \frac{x - \mu}{\sigma}\]

通常认为 \(|Z| > 3\) 为异常值

IQR 方法

  • 异常值 < \(Q_1 - 1.5 \times IQR\)
  • 异常值 > \(Q_3 + 1.5 \times IQR\)

其中 \(IQR = Q_3 - Q_1\)(四分位距)

成交量异常检测

Listing 14
import pandas as pd
import numpy as np

np.random.seed(42)
n_days = 50
df_vol = pd.DataFrame({
    'Date': pd.date_range('2024-01-01', periods=n_days),
    'Price': 100 + np.random.randn(n_days).cumsum() * 0.5,
    'Volume': np.random.randint(1000000, 10000000, n_days),
    'Turnover': np.random.uniform(100000000, 1000000000, n_days)
})

# 添加异常成交量
df_vol.loc[10, 'Volume'] = 100000000   # 异常高
df_vol.loc[25, 'Volume'] = 100         # 异常低
df_vol.loc[40, 'Volume'] = 0           # 零成交

# 异常检测规则
rule1 = df_vol['Volume'] <= 0
volume_mean = df_vol['Volume'].mean()
rule2 = df_vol['Volume'] > volume_mean * 10
rule3 = df_vol['Volume'] < volume_mean * 0.01
anomaly_vol = rule1 | rule2 | rule3

print(f'历史均值: {volume_mean:,.0f}')
print(f'异常记录数: {anomaly_vol.sum()}')
vol_anomalies = df_vol[anomaly_vol].copy()
vol_anomalies['Reason'] = ''
vol_anomalies.loc[rule1, 'Reason'] = '非正成交量'
vol_anomalies.loc[rule2, 'Reason'] = '异常高成交量'
vol_anomalies.loc[rule3, 'Reason'] = '异常低成交量'
print('\n异常记录:')
print(vol_anomalies[['Date', 'Volume', 'Reason']])
历史均值: 7,334,703
异常记录数: 3

异常记录:
         Date     Volume  Reason
10 2024-01-11  100000000  异常高成交量
25 2024-01-26        100  异常低成交量
40 2024-02-10          0  异常低成交量

成交额交叉验证

Listing 15
# 估计成交额 = 价格 × 成交量 × 100(每手100股)
df_vol['Est_Turnover'] = df_vol['Price'] * df_vol['Volume'] * 100
df_vol['Turnover_Ratio'] = df_vol['Est_Turnover'] / df_vol['Turnover']

print('成交额验证(比例应在合理范围内):')
print(df_vol[['Date', 'Turnover', 'Est_Turnover',
              'Turnover_Ratio']].head(10))

# 比例 > 2 或 < 0.5:估计值与实际值相差超过2倍
turnover_anomaly = (
    (df_vol['Turnover_Ratio'] > 2) | (df_vol['Turnover_Ratio'] < 0.5)
)
print(f'\n成交额比例异常记录: {turnover_anomaly.sum()} 条')
成交额验证(比例应在合理范围内):
        Date      Turnover  Est_Turnover  Turnover_Ratio
0 2024-01-01  9.367279e+08  2.290835e+10       24.455719
1 2024-01-02  8.273083e+08  7.093216e+10       85.738481
2 2024-01-03  6.700634e+08  6.204091e+10       92.589609
3 2024-01-04  8.843145e+08  2.456144e+10       27.774554
4 2024-01-05  8.233049e+08  1.061021e+10       12.887342
5 2024-01-06  2.679131e+08  7.634030e+10      284.944299
6 2024-01-07  9.033031e+08  2.549965e+10       28.229339
7 2024-01-08  5.854080e+08  3.302121e+10       56.407167
8 2024-01-09  8.266961e+08  8.096462e+10       97.937580
9 2024-01-10  9.064822e+08  2.798414e+10       30.871145

成交额比例异常记录: 50 条

交叉验证原理:成交量 × 价格 ≈ 成交额。如果比例严重偏离,至少有一个数据是错误的。

数据一致性检验

金融数据存在内在的逻辑约束:

  • \(Price_t > 0\):价格必须为正
  • \(High_t \geq \max(Open_t, Close_t)\):最高价不低于开盘和收盘
  • \(Low_t \leq \min(Open_t, Close_t)\):最低价不高于开盘和收盘
  • \(Return_t = \frac{Price_t - Price_{t-1}}{Price_{t-1}}\):收益率定义

通过检验这些约束是否满足,可以发现数据中的逻辑矛盾。

一致性检验:代码实现

Listing 16
import pandas as pd
import numpy as np

np.random.seed(42)
n_days = 10
index_values = [3000 + i * 10 + np.random.randn() * 5
                for i in range(n_days)]
returns = pd.Series(index_values).pct_change().tolist()

df_index = pd.DataFrame({
    'Date': pd.date_range('2024-01-01', periods=n_days),
    'Index_Close': index_values,
    'Index_Open': [v + np.random.uniform(-10, 10) for v in index_values],
    'Daily_Return': returns,
    'Volume': np.random.randint(100000000, 500000000, n_days)
})

# 人为添加不一致
df_index.loc[5, 'Index_Close'] = 3100
df_index.loc[3, 'Daily_Return'] = -0.5

# 一致性检验
consist1 = df_index['Index_Close'] <= 0
consist2 = ((df_index['Index_Open'] / df_index['Index_Close'] > 1.2) |
            (df_index['Index_Open'] / df_index['Index_Close'] < 0.8))
df_index['Calculated_Return'] = df_index['Index_Close'].pct_change()
consist3 = (df_index['Calculated_Return'] -
            df_index['Daily_Return']).abs() > 0.01
inconsistency_mask = consist1 | consist2 | consist3

print(f'不一致记录数: {inconsistency_mask.sum()}')
result = df_index[inconsistency_mask][
    ['Date', 'Index_Close', 'Daily_Return', 'Calculated_Return']
]
print('\n不一致详情:')
print(result)
不一致记录数: 3

不一致详情:
        Date  Index_Close  Daily_Return  Calculated_Return
3 2024-01-04  3037.615149     -0.500000           0.004755
5 2024-01-06  3100.000000      0.003291           0.020130
6 2024-01-07  3067.896064      0.006254          -0.010356

实战案例:股指数据加载与诊断

Listing 17
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

np.random.seed(42)
n = 50
dates = pd.date_range('2024-01-01', periods=n)
index_values = 3000 + np.arange(n) * 5 + np.random.randn(n) * 20

df_index = pd.DataFrame({
    'Date': dates,
    'Open': index_values + np.random.uniform(-5, 5, n),
    'High': index_values + np.random.uniform(0, 10, n),
    'Low': index_values - np.random.uniform(0, 10, n),
    'Close': index_values,
    'Volume': np.random.randint(100000000, 500000000, n),
    'Amount': np.random.randint(
        100000000000, 500000000000, n, dtype=np.int64)
})

# 人为添加错误
df_index.loc[5, 'Close'] = '3,250.5'          # 千分位
df_index.loc[10, 'Volume'] = '200000000股'     # 含单位
df_index.loc[15, 'Close'] = -3000              # 负价格
df_index.loc[20, 'High'] = df_index.loc[20, 'Low'] - 100  # High < Low
df_index.loc[25, 'Low'] = 3500                 # Low > Close
df_index.loc[30, 'Volume'] = 10000000000       # 异常高成交量
df_index.loc[35, 'Close'] = 5000               # 异常高指数

print('=== 原始数据预览 ===')
print(df_index.head(10))
print('\n数据类型:')
print(df_index.dtypes)
=== 原始数据预览 ===
        Date         Open         High          Low        Close     Volume  \
0 2024-01-01  3006.343525  3017.971004  3006.726482  3009.934283  423839252   
1 2024-01-02  3005.256684  3004.100415  3000.369529  3002.234714  331166767   
2 2024-01-03  3018.699277  3031.879361  3022.546019  3022.953771  205797503   
3 2024-01-04  3050.329466  3050.854020  3039.551668  3045.460597  240071312   
4 2024-01-05  3018.039380  3023.391334  3008.541289  3015.316933  349369215   
5 2024-01-06  3017.304418  3029.278174  3020.151383      3,250.5  393383200   
6 2024-01-07  3056.639477  3064.764291  3056.463326  3061.584256  282708911   
7 2024-01-08  3053.503309  3051.449214  3048.083737  3050.348695  137856186   
8 2024-01-09  3032.679086  3032.889864  3024.158784  3030.610512  148673237   
9 2024-01-10  3058.141273  3060.122279  3054.107537  3055.851201  165293686   

         Amount  
0  204774402025  
1  348807527746  
2  199681546865  
3  236026525614  
4  466612596374  
5  380264498814  
6  212937413097  
7  105680312281  
8  172364485735  
9  238025737608  

数据类型:
Date      datetime64[ns]
Open             float64
High             float64
Low              float64
Close             object
Volume            object
Amount             int64
dtype: object

实战:格式错误修复

Listing 18
df_fixed = df_index.copy()
print('=== 格式错误修复 ===\n')

# 修复1:移除千分位
if df_fixed['Close'].dtype == 'object':
    df_fixed['Close'] = df_fixed['Close'].astype(str).str.replace(',', '')
    print('步骤1: 移除价格列的千分位逗号')

# 修复2:转换为数值
numeric_cols = ['Open', 'High', 'Low', 'Close']
for col in numeric_cols:
    if df_fixed[col].dtype == 'object':
        df_fixed[col] = pd.to_numeric(df_fixed[col], errors='coerce')
        print(f'步骤2: 将{col}转换为数值类型')

# 修复3:成交量清洗
if df_fixed['Volume'].dtype == 'object':
    def clean_vol(vol):
        if pd.isna(vol):
            return np.nan
        vol_str = str(vol).replace('股', '').replace(',', '').replace('万', '0000')
        try:
            return float(vol_str)
        except:
            return np.nan
    df_fixed['Volume'] = df_fixed['Volume'].apply(clean_vol)
    print('步骤3: 清洗成交量格式')

print(f'\n格式修复后数据类型:')
print(df_fixed.dtypes)
=== 格式错误修复 ===

步骤1: 移除价格列的千分位逗号
步骤2: 将Close转换为数值类型
步骤3: 清洗成交量格式

格式修复后数据类型:
Date      datetime64[ns]
Open             float64
High             float64
Low              float64
Close            float64
Volume           float64
Amount             int64
dtype: object

实战:逻辑错误检测与修复

Listing 19
print('=== 逻辑错误检测与修复 ===\n')
errors = []

# 规则1:价格必须为正
error_price_negative = df_fixed['Close'] <= 0
if error_price_negative.any():
    errors.append(('负价格', error_price_negative.sum()))
    df_fixed.loc[error_price_negative, 'Close'] = np.nan
    df_fixed['Close'] = df_fixed['Close'].ffill()
    print(f'规则1: 修复{error_price_negative.sum()}条负价格记录')

# 规则2:High >= Close, Low <= Close
error_high_low = ((df_fixed['High'] < df_fixed['Close']) |
                  (df_fixed['Low'] > df_fixed['Close']))
if error_high_low.any():
    errors.append(('高低价错误', error_high_low.sum()))
    df_fixed.loc[df_fixed['High'] < df_fixed['Close'], 'High'] = \
        df_fixed['Close']
    df_fixed.loc[df_fixed['Low'] > df_fixed['Close'], 'Low'] = \
        df_fixed['Close']
    print(f'规则2: 修复{error_high_low.sum()}条高低价错误记录')

# 规则3:单日涨跌幅不超过15%
df_fixed['Prev_Close'] = df_fixed['Close'].shift(1)
df_fixed['Daily_Change'] = (
    (df_fixed['Close'] - df_fixed['Prev_Close']) / df_fixed['Prev_Close']
)
error_excess_change = df_fixed['Daily_Change'].abs() > 0.15
if error_excess_change.any():
    errors.append(('异常涨跌', error_excess_change.sum()))
    print(f'规则3: 发现{error_excess_change.sum()}条异常涨跌记录')
    print(df_fixed[error_excess_change][
        ['Date', 'Close', 'Prev_Close', 'Daily_Change']])

# 规则4:成交量异常
volume_median = df_fixed['Volume'].median()
error_volume = ((df_fixed['Volume'] > volume_median * 10) |
                (df_fixed['Volume'] < volume_median * 0.01))
if error_volume.any():
    errors.append(('异常成交量', error_volume.sum()))
    print(f'规则4: 发现{error_volume.sum()}条异常成交量记录')

print(f'\n=== 错误汇总 ===')
for error_type, count in errors:
    print(f'{error_type}: {count}条')
=== 逻辑错误检测与修复 ===

规则1: 修复1条负价格记录
规则2: 修复5条高低价错误记录
规则3: 发现2条异常涨跌记录
         Date        Close   Prev_Close  Daily_Change
35 2024-02-05  5000.000000  3186.450898      0.569144
36 2024-02-06  3184.177272  5000.000000     -0.363165
规则4: 发现1条异常成交量记录

=== 错误汇总 ===
负价格: 1条
高低价错误: 5条
异常涨跌: 2条
异常成交量: 1条

实战:数据质量可视化

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# 子图1:指数走势
axes[0, 0].plot(df_fixed['Date'], df_fixed['Close'], 'b-', linewidth=2)
axes[0, 0].set_title('指数收盘价走势', fontsize=12)
axes[0, 0].set_ylabel('点位', fontsize=10)
axes[0, 0].grid(True, alpha=0.3)
if error_excess_change.any():
    axes[0, 0].scatter(
        df_fixed[error_excess_change]['Date'],
        df_fixed[error_excess_change]['Close'],
        color='red', s=50, zorder=5, label='异常涨跌')
    axes[0, 0].legend()

# 子图2:日收益率分布
returns = df_fixed['Daily_Change'].dropna()
axes[0, 1].hist(returns, bins=30, color='steelblue', edgecolor='black')
axes[0, 1].axvline(returns.mean(), color='red', linestyle='--',
                    label=f'均值: {returns.mean():.4f}')
axes[0, 1].axvline(returns.median(), color='green', linestyle='--',
                    label=f'中位数: {returns.median():.4f}')
axes[0, 1].set_title('日收益率分布', fontsize=12)
axes[0, 1].set_xlabel('收益率', fontsize=10)
axes[0, 1].legend(fontsize=9)
axes[0, 1].grid(True, alpha=0.3)

# 子图3:成交量
axes[1, 0].bar(range(len(df_fixed)), df_fixed['Volume'],
               color='coral', edgecolor='black')
axes[1, 0].set_title('成交量', fontsize=12)
axes[1, 0].set_xlabel('交易序号', fontsize=10)
axes[1, 0].grid(True, alpha=0.3, axis='y')

# 子图4:日内振幅
df_fixed['Amplitude'] = (
    (df_fixed['High'] - df_fixed['Low']) / df_fixed['Low'] * 100
)
axes[1, 1].plot(df_fixed['Date'], df_fixed['Amplitude'],
                'g-', linewidth=1.5)
axes[1, 1].axhline(df_fixed['Amplitude'].mean(), color='red',
                    linestyle='--', label='平均振幅')
axes[1, 1].set_title('日内振幅(%)', fontsize=12)
axes[1, 1].set_ylabel('振幅(%)', fontsize=10)
axes[1, 1].legend(fontsize=9)
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print('=== 数据质量指标 ===')
print(f'1. 完整性: 缺失值比例={df_fixed.isnull().sum().sum() / df_fixed.size:.2%}')
print(f'2. 一致性: 逻辑错误已修复')
print(f'3. 准确性: {len(errors)}类错误已处理')
print(f'4. 有效记录: {len(df_fixed)}/{len(df_index)} ({len(df_fixed)/len(df_index):.1%})')
Figure 1: 数据质量可视化分析
=== 数据质量指标 ===
1. 完整性: 缺失值比例=0.40%
2. 一致性: 逻辑错误已修复
3. 准确性: 4类错误已处理
4. 有效记录: 50/50 (100.0%)

本章总结

格式错误处理

  • 日期:pd.to_datetime(errors='coerce')
  • 数值:pd.to_numeric(errors='coerce')
  • 特殊字符:str.replace() 先清洗再转换

逻辑错误处理

  • 基于业务规则定义布尔条件
  • 交叉验证发现不一致
  • 处理策略:删除 / 前值填充 / 标记审核